/**
 * This file is part of the NatCrackerToolbox.
 * 
 * Copyright (C) 2009 - 2011 Peerialism Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 **/
package se.peerialism.natcrackeremu.model.traversal.logic;

import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.StringTokenizer;


import com.peerialism.natcracker.common.FilteringPolicy;
import com.peerialism.natcracker.common.GatewayDevice;
import com.peerialism.natcracker.common.PortMappingTechnique;
import com.peerialism.natcracker.common.TraversalStrategy;
import com.peerialism.natcracker.common.GatewayDevice.GatewayType;
import com.peerialism.natcracker.common.PortAllocationPolicy;
import com.peerialism.natcracker.common.PortMappingPolicy;

public class TraversalLogicV2 {
	static boolean ENABLE_OPTIMIZATIONS = true;

	enum ReqKn {
		NoNeed, KnowEOrAbleToReceiveFromHost, KnowEOrAbleToReceiveFromAny, NeedE
	}

	private static ReqKn checkMapping(GatewayDevice x) {
		ReqKn ret = ReqKn.NoNeed;
		PortMappingPolicy map = x.getPortMappingPolicy();
		if (map == PortMappingPolicy.PORT_DEPENDENT)
			ret = ReqKn.KnowEOrAbleToReceiveFromHost; // Either alloc!=RND or flt!=PD
		return ret;
	}

	private static ReqKn checkFiltering(GatewayDevice x) {
		ReqKn ret = ReqKn.NoNeed;
		FilteringPolicy flt = x.getPortFilteringPolicy();
		PortAllocationPolicy alloc = x.getPortAllocationPolicy();
		if (flt == FilteringPolicy.PORT_DEPENDENT)
			ret = ReqKn.NeedE; // alloc!=RND
		else if (flt == FilteringPolicy.HOST_DEPENDENT && alloc == PortAllocationPolicy.RANDOM)
			ret = ReqKn.KnowEOrAbleToReceiveFromAny; // Either alloc!=RND or flt==EI
		return ret;
	}

	private static boolean satReq(GatewayDevice x, ReqKn reqFlt) {
		boolean ret = true;
		PortAllocationPolicy alloc = x.getPortAllocationPolicy();
		FilteringPolicy flt = x.getPortFilteringPolicy();
		if (reqFlt == ReqKn.NeedE) {
			if (alloc == PortAllocationPolicy.RANDOM)
				ret = false;
		} else if (reqFlt == ReqKn.KnowEOrAbleToReceiveFromHost) {
			if (alloc == PortAllocationPolicy.RANDOM && flt == FilteringPolicy.PORT_DEPENDENT)
				ret = false;
		} else if (reqFlt == ReqKn.KnowEOrAbleToReceiveFromAny)
			if (alloc == PortAllocationPolicy.RANDOM && (flt == FilteringPolicy.HOST_DEPENDENT || flt == FilteringPolicy.PORT_DEPENDENT))
				ret = false;
		return ret;
	}

	public static TraversalStrategy getStrategyFinal(GatewayDevice x1, GatewayDevice x2) {
		PortMappingTechnique str1 = PortMappingTechnique.NONE;
		PortMappingTechnique str2 = PortMappingTechnique.NONE;
		boolean cantConnect = true;
		boolean first = false;
		boolean second = false;
		if ((first = (x1.getPortFilteringPolicy() == FilteringPolicy.ENDPOINT_INDEPENDENT && x1.getPortMappingPolicy() == PortMappingPolicy.ENDPOINT_INDEPENDENT))
				|| (second = (x2.getPortFilteringPolicy() == FilteringPolicy.ENDPOINT_INDEPENDENT && x2.getPortMappingPolicy() == PortMappingPolicy.ENDPOINT_INDEPENDENT))) {
			// One is F=EI && M=EI
			if (first)
				str1 = PortMappingTechnique.ACQUIRE;
			else if (second)
				str2 = PortMappingTechnique.ACQUIRE;
			cantConnect = false;
		} else {
			ReqKn reqFlt1 = checkFiltering(x1);
			ReqKn reqFlt2 = checkFiltering(x2);
			ReqKn reqMap1 = checkMapping(x1);
			ReqKn reqMap2 = checkMapping(x2);
			// Take the most restrictive of req1
			ReqKn req1 = ReqKn.NoNeed;
			if (reqFlt1 == ReqKn.NeedE)
				req1 = ReqKn.NeedE;
			else if (reqFlt1 == ReqKn.KnowEOrAbleToReceiveFromAny)
				req1 = ReqKn.KnowEOrAbleToReceiveFromAny;
			else if (reqMap1 == ReqKn.KnowEOrAbleToReceiveFromHost)
				req1 = ReqKn.KnowEOrAbleToReceiveFromHost;
			// Take the most restrictive of req2
			ReqKn req2 = ReqKn.NoNeed;
			if (reqFlt2 == ReqKn.NeedE)
				req2 = ReqKn.NeedE;
			else if (reqFlt2 == ReqKn.KnowEOrAbleToReceiveFromAny)
				req2 = ReqKn.KnowEOrAbleToReceiveFromAny;
			else if (reqMap2 == ReqKn.KnowEOrAbleToReceiveFromHost)
				req2 = ReqKn.KnowEOrAbleToReceiveFromHost;
			if (satReq(x2, req1) && satReq(x1, req2)) {
				if (ENABLE_OPTIMIZATIONS) {
					StrategyCombination comb = decideStrategy(x1, x2);
					str1 = comb.str1;
					str2 = comb.str2;
				} else if (x1.getPortFilteringPolicy() == FilteringPolicy.ENDPOINT_INDEPENDENT && x2.getPortFilteringPolicy() != FilteringPolicy.PORT_DEPENDENT) {
					str1 = PortMappingTechnique.ACQUIRE;
					str2 = PortMappingTechnique.NONE;
				} else if (x2.getPortFilteringPolicy() == FilteringPolicy.ENDPOINT_INDEPENDENT && x1.getPortFilteringPolicy() != FilteringPolicy.PORT_DEPENDENT) {
					str1 = PortMappingTechnique.NONE;
					str2 = PortMappingTechnique.ACQUIRE;
				} else {
					str1 = k2s(x1);
					str2 = k2s(x2);
					if (x1.getPortAllocationPolicy() == PortAllocationPolicy.RANDOM)
						str1 = PortMappingTechnique.NONE;
					if (x2.getPortAllocationPolicy() == PortAllocationPolicy.RANDOM)
						str2 = PortMappingTechnique.NONE;
				}
				cantConnect = false;
			}
		}
		TraversalStrategy result;
		if (cantConnect)
			result = new TraversalStrategy(!cantConnect);
		else
			result = new TraversalStrategy(str1, str2, 0.0);
		return result;
	}

	private static StrategyCombination decideStrategy(GatewayDevice x1, GatewayDevice x2) {
		// Allocation Policy: ENDPOINT_INDEPENDENT, ENDPOINT_DEPENDENT, PORT_DEPENDENT
		PortMappingPolicy mappA = x1.getPortMappingPolicy();
		PortMappingPolicy mappB = x2.getPortMappingPolicy();
		// Filtering Policy: ENDPOINT_INDEPENDENT, ADDRESS_DEPENDENT, ADDRESS_PORT_DEPENDENT
		FilteringPolicy fltA = x1.getPortFilteringPolicy();
		FilteringPolicy fltB = x2.getPortFilteringPolicy();
		// PortMapping technique: RANDOM, PORT_PRESERVATION, PORT_CONTIGUITY
		PortAllocationPolicy allocA = x1.getPortAllocationPolicy();
		PortAllocationPolicy allocB = x2.getPortAllocationPolicy();
		StrategyCombination comb = new StrategyCombination();
		if (isPP_PP(x1, x2)) {
			// Optimize region 1 in connectivity matrix PP - PP
			// If necessary to know the endpoint, use PP
			boolean assigned = false;
			if (fltA == FilteringPolicy.PORT_DEPENDENT || mappA == PortMappingPolicy.PORT_DEPENDENT) {
				comb.str2 = k2s(x2);
				assigned = true;
			}
			if (fltB == FilteringPolicy.PORT_DEPENDENT || mappB == PortMappingPolicy.PORT_DEPENDENT) {
				comb.str1 = k2s(x1);
				assigned = true;
			}
			if (!assigned)
				// If no strategy defined
				comb.str1 = PortMappingTechnique.PREDICT_PRESERVATION;
		} else if (isPP_PC(x1, x2)) {
			// Optimize region 2 in connectivity matrix PP - PC
			boolean assigned = false;
			// If necessary to know the endpoint, use PC
			if (fltA == FilteringPolicy.PORT_DEPENDENT) {
				comb.str2 = k2s(x2);
				if (mappB == PortMappingPolicy.PORT_DEPENDENT)
					comb.str1 = k2s(x1);
				assigned = true;
			}
			if (fltB == FilteringPolicy.PORT_DEPENDENT) {
				comb.str1 = k2s(x1);
				if (mappA == PortMappingPolicy.PORT_DEPENDENT)
					comb.str2 = k2s(x2);
				assigned = true;
			}
			if (!assigned)
				// Use preservation on the side that supports it
				if (allocA == PortAllocationPolicy.PORT_PRESERVATION)
					comb.str1 = k2s(x1);
				else
					comb.str2 = k2s(x2);
		} else if (isPC_PC(x1, x2)) {
			// Optimize region 3 in connectivity matrix PP - PC
			// Filter EI, use SHP and avoid PC
			if (fltA == FilteringPolicy.ENDPOINT_INDEPENDENT && fltB != FilteringPolicy.PORT_DEPENDENT)
				comb.str1 = PortMappingTechnique.ACQUIRE;
			else if (fltB == FilteringPolicy.ENDPOINT_INDEPENDENT && fltA != FilteringPolicy.PORT_DEPENDENT)
				comb.str2 = PortMappingTechnique.ACQUIRE;
			else {
				boolean assigned = false;
				if (fltA == FilteringPolicy.PORT_DEPENDENT || mappA == PortMappingPolicy.PORT_DEPENDENT)
					comb.str2 = k2s(x2);
				if (fltB == FilteringPolicy.PORT_DEPENDENT || mappB == PortMappingPolicy.PORT_DEPENDENT)
					comb.str1 = k2s(x1);
				if (!assigned)
					comb.str1 = k2s(x1);
			}
		} else // Use preservation when possible
			if (allocA == PortAllocationPolicy.PORT_PRESERVATION)
				comb.str1 = k2s(x1);
			else if (allocB == PortAllocationPolicy.PORT_PRESERVATION)
				comb.str2 = k2s(x2);
			else
				// Use SHP when possible
				if (allocA == PortAllocationPolicy.RANDOM && fltA == FilteringPolicy.ENDPOINT_INDEPENDENT)
					comb.str1 = k2s(x1);
				else if (allocB == PortAllocationPolicy.RANDOM && fltB == FilteringPolicy.ENDPOINT_INDEPENDENT)
					comb.str2 = k2s(x2);
				else
					// Use PC if it's really needed
					if (allocA == PortAllocationPolicy.PORT_CONTIGUITY)
						comb.str1 = k2s(x1);
					else if (allocB == PortAllocationPolicy.PORT_CONTIGUITY)
						comb.str2 = k2s(x2);
		return comb;
	}

	public static boolean isPP_PP(GatewayDevice x1, GatewayDevice x2) {
		if (x1.getPortAllocationPolicy() == PortAllocationPolicy.PORT_PRESERVATION && x2.getPortAllocationPolicy() == PortAllocationPolicy.PORT_PRESERVATION)
			return true;
		return false;
	}

	public static boolean isPP_PC(GatewayDevice x1, GatewayDevice x2) {
		if (x1.getPortAllocationPolicy() == PortAllocationPolicy.PORT_PRESERVATION && x2.getPortAllocationPolicy() == PortAllocationPolicy.PORT_CONTIGUITY)
			return true;
		else if (x1.getPortAllocationPolicy() == PortAllocationPolicy.PORT_CONTIGUITY && x2.getPortAllocationPolicy() == PortAllocationPolicy.PORT_PRESERVATION)
			return true;
		return false;
	}

	public static boolean isPC_PC(GatewayDevice x1, GatewayDevice x2) {
		if (x1.getPortAllocationPolicy() == PortAllocationPolicy.PORT_CONTIGUITY && x2.getPortAllocationPolicy() == PortAllocationPolicy.PORT_CONTIGUITY)
			return true;
		return false;
	}

	private static PortMappingTechnique k2s(GatewayDevice x) {
		PortMappingTechnique str = PortMappingTechnique.NONE;
		if (x.getPortAllocationPolicy() == PortAllocationPolicy.PORT_CONTIGUITY)
			str = PortMappingTechnique.PREDICT_CONTIGUITY;
		else if (x.getPortAllocationPolicy() == PortAllocationPolicy.PORT_PRESERVATION)
			str = PortMappingTechnique.PREDICT_PRESERVATION;
		else if (x.getPortAllocationPolicy() == PortAllocationPolicy.RANDOM)
			str = PortMappingTechnique.ACQUIRE;
		return str;
	}

	public static void main(String[] args) {
		GatewayDevice x1 = new GatewayDevice();
		x1.setGatewayType(GatewayType.NAT);
		GatewayDevice x2 = new GatewayDevice();
		x2.setGatewayType(GatewayType.NAT);
		PortAllocationPolicy[] mts = PortAllocationPolicy.values();
		FilteringPolicy[] fps = FilteringPolicy.values();
		GatewayDevice[] gws = new GatewayDevice[mts.length * fps.length * PortMappingPolicy.values().length];
		int i = 0;
		for (PortAllocationPolicy mt : mts)
			for (FilteringPolicy fp : fps)
				for (PortMappingPolicy m : PortMappingPolicy.values()) {
					GatewayDevice gatewayDevice1 = new GatewayDevice();
					gatewayDevice1.setPortMappingPolicy(m);
					gatewayDevice1.setPortFilteringPolicy(fp);
					gatewayDevice1.setPortAllocationPolicy(mt);
					gatewayDevice1.setGatewayType(GatewayType.NAT);
					gws[i] = gatewayDevice1;
					i++;
				}
		FileWriter f;
		try {
			f = new FileWriter("natBig.html");
			f.write("<TABLE border='1'>");
			f.write("<TR>");
			f.write("<TD>--</TD><TD>A</TD><TD></TD>");
			for (GatewayDevice gw : gws)
			{
				f.write("<TD><B>");
				f.write(removeDash(GatewayDevice.shorten(gw.getPortAllocationPolicy().name(), "_")) + "");
				f.write("</TD></B>");
			}
			f.write("</TR>");
			f.write("<TR>");
			f.write("<TD>B</TD><TD></TD><TD></TD>");
			for (GatewayDevice gw : gws)
			{
				f.write("<TD><B>");
				f.write(removeDash(GatewayDevice.shorten(gw.getPortFilteringPolicy().name(), "_")) + "");
				f.write("</TD></B>");
			}
			f.write("</TR>");
			f.write("<TR>");
			f.write("<TD></TD><TD></TD><TD></TD>");
			for (GatewayDevice gw : gws)
			{
				f.write("<TD><B>");
				f.write(removeDash(GatewayDevice.shorten(gw.getPortMappingPolicy().name(), "_")) + "");
				f.write("</TD></B>");
			}
			f.write("</TR>");
			HashSet<String> done = new HashSet<String>();
			for (int r = 0; r < gws.length; r++) {
				f.write("<TR>");
				// f.write("<TD></TD><TD></TD>");
				f.write("<TD><B>" + removeDash(GatewayDevice.shorten(gws[r].getPortAllocationPolicy().name(), "_")) + "</B></TD>");
				f.write("<TD><B>" + removeDash(GatewayDevice.shorten(gws[r].getPortFilteringPolicy().name(), "_")) + "</B></TD>");
				f.write("<TD><B>" + removeDash(GatewayDevice.shorten(gws[r].getPortMappingPolicy().name(), "_")) + "</B></TD>");
				for (int c = 0; c < gws.length; c++) {
					String cur = (c <= r) ? r + "-" + c : c + "-" + r;
					if (!done.contains(cur))
						done.add(cur);
					else {
						f.write("<TD/>");
						continue;
					}
					TraversalStrategy s = getStrategyFinal(gws[r], gws[c]);
					String color = s.canConnect() ? "green" : "red";
					String entry = (s.canConnect() ? fromRealToModelStr(removeDash(GatewayDevice.shorten(s.getDownloaderStrategy().name(), "_"))) : "N") + "-"
					+ (s.canConnect() ? fromRealToModelStr(removeDash(GatewayDevice.shorten(s.getUploaderStrategy().name(), "_"))) : "N");
					f.write("<TD bgcolor='" + bgColor(entry) + "'><font color='" + color + "'>");
					f.write("" + entry + "</font><BR/>");
					f.write("</TD>");
				}
				f.write("</TR>");
			}
			f.write("<TABLE>");
			f.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public static String bgColor(String str) {
		if (str.equals("G-H") || str.equals("H-G"))
			return "lightgreen";
		if (str.contains("P-G") || str.contains("G-P"))
			return "yellow";
		if (str.contains("G-C") || str.contains("C-G"))
			return "cyan";
		if (str.contains("P-P"))
			return "red";
		if (str.contains("C-C"))
			return "blue";
		if (str.contains("C-P") || str.contains("P-C"))
			return "magenta";
		return "";
	}

	public static String fromRealToModelStr(String str) {
		if (str.contains("N"))
			return "G";
		if (str.contains("A"))
			return "H";
		if (str.contains("PP"))
			return "P";
		if (str.contains("PC"))
			return "C";
		return "";
	}

	public static String removeDash(String str) {
		StringTokenizer tok = new StringTokenizer(str, "_");
		if (tok.countTokens() == 2)
			return tok.nextToken() + tok.nextToken();
		return str;
	}
}

class StrategyCombination {
	PortMappingTechnique str1 = PortMappingTechnique.NONE;
	PortMappingTechnique str2 = PortMappingTechnique.NONE;
}
